#!/bin/sh
#   The above line causes this script to be run using the Bourne Shell (sh)
#######################################################################
#
#   Script to maintain a contacts database.
#
#######################################################################

#
#   Define the name of the file
#
fname=names.dat

#
#   Set a name for all temp files used during this script.
#   The `basename $0` bit causes the temp filename to include the filename
#   of this script.  The $$ bit causes the name to include a number unique
#   to this shell, in case more than one copy of this script is running at
#   any given time.
#
#   E.g.  /tmp/menu.12345
#
tmpfile=/tmp/`basename $0`.$$

#
#   pause()
#
#   Ask the user to press ENTER and wait for them to do so
#
pause()
{
    echo "Hit <ENTER> to continue: \c"
    read junk
}

#
#   yesno()
#
#   A function to display a string (passed in as $*), followed by a "(Y/N)?",
#   and then ask the user for either a Yes or No answer.  Nothing else is
#   acceptable.
#   If Yes is answered, yesno() returns with an exit code of 0 (True).
#   If No is answered, yesno() returns with an exit code of 1 (False).
#
yesno()
{
    #
    #   Loop until a valid response is entered
    #
    while :
    do
        #
        #   Display the string passed in in $1, followed by "(Y/N)?"
        #   The \c causes suppression of echo's newline
        #
        echo "$* (Y/N)? \c"

        #
        #   Read the answer - only the first word of the answer will
        #   be stored in "yn".  The rest will be discarded
        #   (courtesy of "junk")
        #
        read yn junk

        case $yn in
            y|Y|yes|Yes|YES)
                return 0;;        # return TRUE
            n|N|no|No|NO)
                return 1;;        # return FALSE
            *)
                echo Please answer Yes or No.;;
                #
                # and continue around the loop ....
                #
        esac
    done
}

#
#   usage
#
#   Generic function to display a usage message and exit the program
#   The ^G is the bell (beep) character
#   "basename" is used to transform "/home/mark/contacts"
#   into "contacts" (for example)
#
usage()
{
    script=$1
    shift

    echo "Usage: `basename $script` $*" 1>&2
    exit 2
}

#
#   quit()
#
#   Prompt the user to exit the program.
#   If they choose to, an exit code is provided in $1 (the first argument)
#
quit()
{
    #
    #   Store the exit code away, coz calling another function
    #   overwrites $1.
    #
    code=$1

    if yesno "Do you really wish to exit"
    then
        exit $code           #  exit using the supplied code.
    fi
}

#
#   heading()
#
#   Display the standard heading used when displaying records
#
heading()
{
    clear

    echo "First Name    Surname         Address             City           State Zip"
    echo "============================================================================"
}

#
#   print_records()
#
#   Read records from standard input, and nicely format each on one line.
#   This function would typically be the target of a pipe.
#   E.g.
#       grep pattern file | print_records
#
#   Used to ensure that all displays of the records are consistent.
#   If this format changes, don't forget to change the "heading" function above
#
print_records()
{
    #
    #   Read from standard input (usually some other program) and
    #   loop once for each line in the input.
    #
    sort -t : +1 | while read aline
    do
        #
        #   Display the line correctly formatted.
        #   Use awk for the formatting.
        #   The "-F :" bit causes awk to perceive fields as being
        #   separated by colons.
        #   "%-14.14s" means display a string in a field width of 14,
        #   left-justified.
        #
        echo $aline | awk -F : '{printf("%-14.14s%-16.16s%-20.20s%-15.15s%-6.6s%-5.5s\n", $1, $2, $3, $4, $5, $6)}'
    done
}

#
#   do_create()
#
#   Create records for our database
#
do_create()
{
    #
    #   Loop until the user is sick of entering records
    #
    while :
    do
        #
        #   Inner loop:  loop until the user is satisfied with
        #   this ONE record
        #
        while :
        do
            #
            #   Read in the contact details from the keyboard
            #
            clear

            echo "Please enter the following contact details:"
            echo
            echo "Given name: \c"
            read name
            echo "   Surname: \c"
            read surname
            echo "   Address: \c"
            read address
            echo "      City: \c"
            read city
            echo "     State: \c"
            read state
            echo "       Zip: \c"
            read zip

            #
            #   Now confirm ...
            #
            clear

            echo "You entered the following contact details:"
            echo
            echo "Given name: $name"
            echo "   Surname: $surname"
            echo "   Address: $address"
            echo "      City: $city"
            echo "     State: $state"
            echo "       Zip: $zip"
            echo

            if yesno Are these details correct
            then
                #
                #   Enter the details onto the end of file,
                #   with the fields separated by colons, and
                #   break out of the inner loop
                #
                echo $name:$surname:$address:$city:$state:$zip >> $fname
                break
            fi
        done

        #
        #   Ask the user if they wish to create another record.
        #   The "break" bit will only be executed if the user
        #   answers "No"
        #
        yesno Create another record || break
    done
}

#
#   do_view()
#
#   Display all records in the file, complete with headings, sorted,
#   one page at a time
#
do_view()
{
    clear

    #
    #   Show what's currently in the file
    #
    (
        heading

        #
        #   Display the lines correctly formatted, using out print_records
        #   function
        #
        cat $fname | print_records
    ) | more

    #
    #  And display how many there are
    #
    echo
    echo There are `cat $fname | wc -l` contacts in the database
}

#
#   do_search()
#
#   Prompt the user for a pattern to search for in the file, and display all
#   results nicely formatted, with headings.
#   It is possible to display ALL records by entering an empty search string.
#   If any records are found, do_search() returns with an exit code of 0 (True)
#   If no records are found, do_search() returns with an exit code of 1 (False)
#   These exit codes are used by do_delete()
#
do_search()
{
    echo "Please enter pattern to search for (ENTER for all): \c"
    read string

    echo

    #
    #   Check to see if there records are in the file matching the pattern.
    #   If there are, "grep" will return 0 (True), which we can use in
    #   the "if".
    #   Send the results to "/dev/null" (trash them), coz we don't actually
    #   want to see them yet.
    #
    if grep "$string" $fname > /dev/null
    then
        #
        #   Use the round brackets to group both the headings and the
        #   actual records together for the pager program
        #
        (
            heading

            #
            #   Search the file again, and this time send the
            #   output to "print_record" for formatting,
            #   via "sort" for sorting
            #
            grep "$string" $fname | print_records
        ) | more
        return 0             # we found some records
    else
        echo "Sorry, no records in file \"$fname\" contain \"$string\""
        return 1             # we didn't find any records
    fi
}

#
#   do_delete()
#
#   Use the search function to find all records matching a certain pattern,
#   prompt the user for a confirmation, then delete those records.
#   It is possible to delete ALL records by entering an empty search string.
#
do_delete()
{
    #
    #   Search for records in the standard manner.
    #   If any records are found, "do_search" will return 0 (true)
    #   allowing us to progress to the "yesno" bit, which will ask us
    #   if we actually want to delete the records.  If we answer "No",
    #   we will be forced to move on to the "return" bit, exiting this
    #   function.  Otherwise, we stay here to delete the records.
    #
    #   A side-effect of calling the "do_search" function is that the
    #   "$string" variable gets set.  We will use that here in this
    #   function when we do the delete.
    #
    do_search && yesno "\nDelete ALL these records" || return

    #
    #   Now delete the records.
    #   Check if they are deleting ALL the records ($string is empty)
    #
    if [ "$string" = "" ]
    then
        #
        #   Simply clear the file
        #
        > $fname

        echo "All records deleted from file \"$fname\""
    else
        #
        #   Actually delete the records, using "sed".
        #   "sed" doesn't actually change the file itself, so we have
        #   to use a temporary intermediate file.
        #
        sed "/$string/d" $fname >> $tmpfile
        mv $tmpfile $fname

        echo "All records containing text \"$string\" deleted from file \"$fname\""
    fi
}

#
#   START MAIN CODE HERE
#

#
#   For the duration of the program, ensure that signals 2 and 3
#   (CNTRL-C and CNTRL-\) cause the program to quit via the "quit" function
#   (as opposed to just dying).
#   We shall pass "quit" an argument of 3, so that if the user presses
#   CNTRL-C, we return an exit code to the parent shell of 3.
#   Why?  Just in case.
#    15 (SIGTERM) is the result of a "kill" or the system shutting down
#     1 (SIGHUP) is the result of a broken connection/terminated login shell
#    No confirmation required, so just use "exit"
#
trap "quit 3" 2 3
trap "exit 0" 1 15

#
#   Check that there is exactly one argument.
#   "$#" contains the number of command line arguments.
#   "$0" is the name of the shell script (complete path as
#   typed in on the command line (e.g. "/home/mark/menu")
#
[ $# == 1 ] || usage $0 filename         # exits the program

#
#   If we got here, they must have supplied a filename.  Store it away
#
fname=$1

#
#   Check if the filename represents a valid file.
#
if [ ! -f $fname ]
then
    echo $1 does not exist

    #
    #   Ask if it should be created
    #
    if yesno "Create it"
    then
        #
        #   Attempt to create it
        #
        > $fname

        #
        #   Check if that succeeeded
        #
        if [ ! -w $fname ]
        then
            echo $1 could not be created
            exit 2
        fi
        #
        #   Otherwise we're OK
        #

    else
        #
        #   They didn't want to create it
        #
        exit 0
    fi
elif [ ! -w $fname ]    # it exists - check if it can be written to
then
    echo Could not open $1 for writing
    exit 2
fi

#
#   Loop forever - until they want to exit the program
#
while true
do
    #
    #   Display the menu
    #
    clear
    echo "\n\t\tSHELL PROGRAMMING DATABASE"
    echo "\t\t\tMAIN MENU"
    echo "\nWhat do you wish to do?\n"
    echo "\t1.  Create records"
    echo "\t2.  View records"
    echo "\t3.  Search for records"
    echo "\t4.  Delete records that match a pattern"
    echo

    #
    #   Prompt for an answer
    #
    echo "Answer (or 'q' to quit)? \c"
    read ans junk

    #
    #   Empty answers (pressing ENTER) cause the menu to redisplay,
    #   so .... back around the loop
    #   We only make it to the "continue" bit if the "test"
    #   program ("[") returned 0 (True)
    #
    [ "$ans" = "" ] && continue

    #
    #   Decide what to do
    #
    case $ans in
        1)     do_create;;
        2)     do_view;;
        3)     do_search;;
        4)     do_delete;;
        q*|Q*) quit 0;;
        *)     echo "please enter a number between 1 and 4";;
    esac

    #
    #   Pause to give the user a chance to see what's on the screen
    #
    pause
done
